【翻译】建立你的第一个 WebAssembly 应用

58373003_p0


当我第一次听见 WebAssembly 消息的时候就觉得它非常酷,我迫不及待地想要开始尝试使用它。然而当我不久之后开始使用 WebAssembly 的时候 ,它给我我最大的感受却是令人泄气。这篇文章的目的就是让你避免这些令人沮丧的部分。

读者须知

这篇文章写于 2016 年六月 24 号, WebAssembly 还是一个非常新并且不稳定的技术,随着 WebAssembly 在各个浏览器中形成规范,这篇文章里的说的所有东西都有可能变成错误的。

有了这些说明……

到底什么是 WebAssembly ?

嗯,官方网站的说明就是这样的

WebAssembly or wasm is a new portable, size- and load-time-efficient format suitable for compilation to the web.

蛤?什么?什么的格式?文本?二进制?老实讲这就是个垃圾说明。
所以,拿出你的小笔记本儿,我会用上我关于 wasm 的一切经验来给出我自己对 Wasm 的说明

WebAssembly 或者 wasm 是一个用于撰写高性能,浏览器无关的网页组件的字节码规范

所以我们还不能一句话说清楚它到底哪里好,不过这里就是说明的余下的部分。
WebAssmbly 通过使用比运行时动态类型变量更高效的静态类型变量引用,实现了性能上的提升。它是由 W3C Community Group 开发,最终能被遵循规范的浏览器兼容。而 WebAssembly 的杀手级特性就是,你最终将可以用任意的语言都来编写这些网页组件

现在听起来是不是厉害多了?

让我们开始吧

每当要开始学习一项新的事物,我通常会寻找一个最小的可行范例来研究它是如何工作的。不幸的是,对我们来说还没有实际的 WebAssembly 的例子。就目前状况来看, wasm 基本上只是一个字节码规范而已。假设我们回到 1996年,一些来自 Sun 软件公司的工程师在介绍 JVM……不是 Java 。我想象中那个会议可能有点像这样
“嘿,大伙儿来看看我们造的这个字节码虚拟机”
“雕,那我们该如何写代码使用它?”


HelloWorld in bytecode

“嗯……厉害..,我会找个时间来看看它的”
“吼啊,请务必让我们知道你的想法,要是你用的时候出了什么差错,记得到我们的 github 主页上给我们留个言”
“你说得太对了,我们还是去看看 github 里的其他项目吧”

即使这个例子因为 JVM 是基于 java 的,所以是一个坏的例子,但我希望你能明白个中重点
如果你的字节码不能在那个场景里被编译它的工具展示出来,你就会有一个非常尴尬的时刻了。所以说,我们到底要如何开始它呢?

在 WebAssembly 之前我们需要知道

大多数技术都是某种革新的结果,尤其是一个合理的尝试会使之逐渐成为一个正式的规范。 Wasm 也没有什么不同,因为它实际上是对 asm.js 所做工作的一种延续。其中 asm.js 是通过静态类型,使之可以被编译的一种javascript 组件编写方法规范。而 Wasm 延续了这个想法,通过制定一个字节码规范使得 Wasm 可以从任何语言编译而来。随着时间的推移,有许多主流浏览器选择传输二进制文件来代替编码过的文本,而不仅仅是 Mozilla 这样做。

asm.js 只是一个使用 javascript 最小语言特性子集的 javascript 的编写规范。你完全可以手写一个简单的 asm.js 例子,如果你愿意弄脏你的手,这就是一个开始学习的好方法(最好在稍后把这些放到一个单独的文件中,并按照惯例使用 your-module-name.asm.js 命名)

1
2
3
4
5
6
7
8
9
function MyMathModule(global) {
"use asm";
var exp = global.Math.exp;
function doubleExp(value) {
value = +value;
return +(+exp(+value) * 2.0);
}
return { doubleExp: doubleExp };
}

这不是一个非常有用的函数,不过它是符合规范的。如果你觉得这看起来有点傻,你不是一个人,不过几乎这里每一个字符都是必须的。所有这些一元操作符号 ‘+’ 是作为类型说明而存在,让编译器知道那些带有’+’符号的变量是双精度类型,而无需在运行时才去推断出他们是什么类型。他的语法非常严苛,不过假如你弄错了写什么,火狐中的控制台会给你一个错误原因来告诉你哪里运行出错了。

如果你想在浏览器中运行它,可以像这样去运行它

1
2
3
4
var myMath = new MyMathModule(window);
for(var i = 0; i < 5; i++) {
console.log(myMath.doubleExp(i));
}

如果你没做错任何事,那输出应该看是像这样的

这就是 WebAssembly 了

现在我们有一个可用的 asm.js 的部件了,我们可以使用由 WebAssembly github page 提供的工具将他编译成 wasm。现在你需要自己去去克隆这个仓库并构建这个工具。这是最糟糕的一部分。因为这些工具仍在不断地开发中,因此不时出现断层式改动都是司空见惯的事情。尤其是当你在 window 环境下的时候。

无论你是在 windows 还是 mac ,你都会需要 make 和 cmake 这两个命令行工具安装在你的系统中。如果你在 windows ,你还需要安装上 visual studio 2015 。如果你在 mac 环境中,请查看这些说明,同样的如果你是在 windows 环境中查看这些说明


在 windows 中构建二进制文件

直接分发该工具的可用二进制文件会是 WebAssmbly 团队迈向正确方向的重要一步。

如果你设法让编译成功,你会看见这个目录下有一个 bin 文件夹被创建,里面包含了一些我们可以用来编译我们的 asm.js 文件为 wasm 的工具。
第一个工具是 asm2wasm.exe。这个工具将 asm.js 代码编译成 .s 的格式化代码,这个.s 里面是抽象语法树(AST)的文本表述,用于 wasm 的。一旦你运行这个工具,最终结果会是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(module
(memory 256 256)
(export "memory" memory)
(type $FUNCSIG$dd (func (param f64) (result f64)))
(import $exp "global.Math" "exp" (param f64) (result f64))
(export "doubleExp" $doubleExp)
(func $doubleExp (param $0 f64) (result f64)
(f64.mul
(call_import $exp
(get_local $0)
)
(f64.const 2)
)
)
)

以后有机会我们可以逐句逐句解析它,但就现在而言,我只是想要向你展示它是什么样的。既然说到 wasm 是一个字节码格式,想要像你对其他 javascript 代码做的那样,直接右键点击并查看代码是不行的。他看上去会更像上面的字节代码。目前的计划是只有当你在 wasm 组件里查看源码时,wasm 才会解析这个字节码,让它变得可被人类阅读的。

我们下一件需要做的事是进一步编译这个 .s 格式的代码为 wasm 二进制代码,我们会用到 wasm-as.exe 将其汇编化。运行了这个文件之后,你最终会的到一份实际上需要用到浏览器的 wasm 字节码。

将 asm.js 编译成 wasm 字节码

然后,拿出你手上的 firefox 或者 chrome canary 的最新副本,启用 WebAssmbly 特性

在火狐里。你要去输入about:config 到地址栏中,然后告诉它你会小心的。在这之后,输入 wasm 到搜索栏中并双击 javascript.potions.wasm 直到它的值变成 true 然后重启火狐浏览器

对于 Chrome Canary 来说,你需要输入 chrome://flags 到地址栏中并向下滚动直到你找的 Experimental WebAssembly ,点击启用链接并重启浏览器。

最后一步就是将这个 wasm 模块跑在浏览器中。 这是另一个痛苦的地方,因为当我第一次尝试的时候这完全是隐藏起来的,我从规范中找不到任何关于调用 wasm 模块的 javascript API 说明。最终,我只好打开 Chrome Canary 的控制台然后敲入 WebAsse 结果并没有任何相关信息弹出来。接下来我尝试敲入Was ,结果居然有了!。看起来这个对象仍处于最缺失文档的初始阶段。这件事发生在我用另一个工具(emscripten)去编译 Wasm 的时候,关于我具体做了什么的那是另一篇博客的主题了。不管怎么说,在我做了那些事了以后,我能够做出一个可用的例子了。

在我到处点击,并最终决定设计一个并 repo 给 WebAssembly 的时候 ,我看见一个名叫 Js.md 的文件,于是我点开了它,不用说,那里他娘的当然有一个现成的带着文档的 javascript Api 。最引人注意的斜体文字在这个文档的顶部。结果的是这文档最好的部分是在非常底部的一小部分,它非常简单的向你展示了如何加载一个模块。我唯一需要做的就是将其中相应的部分替换掉并尝试用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fetch("my-math-module.wasm")
.then(function(response) {
return response.arrayBuffer();
})
.then(function(buffer) {
var dependencies = {
"global": {},
"env": {}
};
dependencies["global.Math"] = window.Math;
var moduleBufferView = new Uint8Array(buffer);
var myMathModule = Wasm.instantiateModule(moduleBufferView, dependencies);
console.log(myMathModule.exports.doubleExp);
for(var i = 0; i < 5; i++) {
console.log(myMathModule.exports.doubleExp(i));
}
});

把这些折腾进一个 html 文件中,在你的本地文件夹中搭建一个服务器,然后加载它。
这救是我在两个浏览器所能看到的结果:

wasm 跑在浏览器中(至少尝试过了)

我猜是时候去写一个 bug 报告了。记住,这完全是个实验性的技术并且非常不稳定,所以当发生像这样事情的时候,千万不要过分沮丧。

恭喜你!

你已经创建了你的第一个 WEbAssembly 组件了,下一步是什么?我们只是简单的在表面掠过而已。手写 asm.js 就是这个例子中挺重要的部分,但是要做有任何非凡的事情都需要很长的时间和非常多的耐心。使用 emscripten 来编译一个非凡的 asm.js 小应用相比之下要简单太多了。在那个 javascript Api 文档中。我十分建议你阅读有关 asm.js 的规范,尤其是关于内存模块描述,因为其中有许多概念都转移到 WebAssembly 当中了。
另一个笑话就是现在你还不能直接传递一个数组当作函数的参数。这里中的一些协议明显应当被修改的,但是在这个规范中还没有明显的改动。去加上你的看法吧

其他需要注意的是,当你开始用 Wasm 做些不平凡的事情的时候,你可能会发现在 WebAssemly 中的实际效果比原生 js 代码还慢。只需记得的是现代 javascript 引擎在编译 javascript 的过程中经过了高度的优化的而这些差距是需要给 wasm 汇编一些时间迎头赶上。WebAssembly 真的还没准备好用于生产环境。

如果你有什么问题关于 wasm 或者是这里提到的一些工具,请到 Stack Overflow 提问并加上适当的标签。